import java.io.File;
import java.io.FileNotFoundException;
import java.util.*;
import java.awt.Point;

/**
 * Board class.
 * 
 */
public class Board {
	Integer[] prevMove;
	ArrayList<Block> blocks;
	private Board parentConfig = null;
	private int[][] simpleBoard;
	private int rows = 4, cols = 5; // board size is 4 rows by 5 columns
	private int emptyBlock = -1;

	// Board class constructors
	/**
	 * Constructor for the Board class. Reads in a Scanner object that
	 * initializes the puzzle board size.
	 * 
	 * @param myScanner
	 *            Specifies board configuration.
	 */
	public Board(Scanner thisScanner) {
		// grab all the blocks from the input configuration
		blocks = new ArrayList<Block>();
		linesToBlocks(thisScanner);

		// translate to the simpler Board, then update the Board
		simpleBoard = new int[rows][cols];
		updateBoard();
	}

	/**
	 * Board class constructor. Creates a copy of the input board for later.
	 * 
	 * @param sameBoard
	 *            Creates an identical Board.
	 */
	public Board(Board sameBoard) {
		// translate to simpler board
		simpleBoard = new int[rows][cols];

		// copy all the blocks
		blocks = new ArrayList<Block>(sameBoard.blocks.size());
		for (Iterator<Block> i = sameBoard.blocks.iterator(); i.hasNext();) {
			Block currBlock = i.next();
			blocks.add(new Block(currBlock.blockId, currBlock.length,
					currBlock.width, currBlock.row, currBlock.column));
		}

		// create an identical simple board
		for (int i = 0; i < sameBoard.rows; i++) {
			for (int j = 0; j < sameBoard.cols; j++) {
				simpleBoard[i][j] = sameBoard.simpleBoard[i][j];
			}
		}
	}

	// End of Board class constructors

	/**
	 * Sorts the blocks. This allows for faster searching in the config History.
	 * 
	 * @param toSort
	 *            ArrayList of Blocks to be sorted
	 * 
	 */
	public String[] sortBlocks(ArrayList<Block> toSort) {
		String[] sortedBlocks = new String[toSort.size()];
		for (int i = 0; i < toSort.size(); i++) {
			sortedBlocks[i] = Integer.toString(toSort.get(i).getLength()) + " "
					+ Integer.toString(toSort.get(i).getWidth()) + " "
					+ Integer.toString(toSort.get(i).getRow()) + " "
					+ Integer.toString(toSort.get(i).getCol());
		}
	
		// use the built in Java Quicksort to sort the blocks
		Arrays.sort(sortedBlocks);
		return sortedBlocks;
	}

	/**
	 * Returns a hash code of the board. For storage in config History.
	 */
	public int hashCode() {
		String myTemp = "";
		String[] sortedBlocks = sortBlocks(blocks);
		for (int i = 0; i < sortedBlocks.length; i++) {
			myTemp += sortedBlocks[i];
		}
		return myTemp.hashCode();
	}

	/**
	 * Constructs an ArrayList of Integer arrays from an input configuration
	 * file. The input scanner object must be formatted in lines of four numbers
	 * that correspond to the description of one block per line, including
	 * width, height and row and column position. Each element in the ArrayList
	 * of Integer arrays represents a block, and the Integer array for each
	 * block is the description, denoted by the four integers.
	 * 
	 * @param myScanner
	 *            Input file from which to extract the blocks
	 */
	private void linesToBlocks(Scanner myScanner) {
		// gives a unique id for each block
		int i = 0;
	
		// scan in a configuration that has a block description per line; width
		// height row column
		while (myScanner.hasNext()) {
			int height = myScanner.nextInt();
			int width = myScanner.nextInt();
			int row = myScanner.nextInt();
			int col = myScanner.nextInt();
	
			blocks.add(new Block(i, height, width, row, col));
			i++;
		}
	}

	/**
	 * Refreshes the board according to the new specifications per block.
	 * 
	 */
	private void updateBoard() {
		// reset an empty simple board
		for (int r = 0; r < rows; r++) {
			for (int c = 0; c < cols; c++) {
				simpleBoard[r][c] = emptyBlock;
			}
		}
	
		// update each block info
		for (Iterator<Block> i = blocks.iterator(); i.hasNext();) {
			Block blockie = i.next();
			int startRow = blockie.getRow();
			int startCol = blockie.getCol();
			int length = blockie.getLength() + startRow;
			int width = blockie.getWidth() + startCol;
			for (int r = startRow; r < length; r++) {
				for (int c = startCol; c < width; c++) {
					simpleBoard[r][c] = blockie.blockId;
				}
			}
		}
	}

	/**
	 * Move method moves the inputted block, identified by its id and position.
	 * Returns the new Board that resulted from moving the block. Uses the
	 * sameBoard constructor to update the new Board after the move and after
	 * recording the move in moves.
	 * 
	 * @param blockIndex
	 *            Specifies the index of the block to be moved
	 * @param rows
	 *            The amount of rows to shift
	 * @param cols
	 *            The amount of cols to shift
	 * @return Returns the new board with the newly shifted block
	 */
	public Board move(int blockIndex, int rows, int cols) {
		Board afterBoard = new Board(this);
		Block block = afterBoard.getBlock(blockIndex);
	
		int row = block.getRow();
		int column = block.getCol();
		int length = block.getLength();
		int width = block.getWidth();
		int id = block.blockId;
	
		// new position
		int newRow = row + rows;
		int newCol = column + cols;
	
		if (!afterBoard.checkVacancy(block.blockId, newRow, newCol, length, width)) {
			return null;
		} else {
	
			// to print the moves later
			afterBoard.prevMove = new Integer[5];
			afterBoard.prevMove[0] = block.row;
			afterBoard.prevMove[1] = block.column;
			afterBoard.prevMove[2] = newRow;
			afterBoard.prevMove[3] = newCol;
			afterBoard.prevMove[4] = id;
	
			block.row = newRow;
			block.column = newCol;
			afterBoard.updateBoard();
		}
		return afterBoard;
	}

	/**
	 * isGoal checks if the input board is equal to the goal configuration by
	 * looking at each block position.
	 * 
	 * @param goal
	 *            Board object with goal configuration
	 * @return True if board configuration is same as specified goal
	 *         configuration
	 * 
	 */
	public boolean isGoal(Board goal) {
		for (Iterator<Block> i = goal.blocks.iterator(); i.hasNext();) {
			Block block = i.next();
			int id = simpleBoard[block.row][block.column];
			if (id == -1) {
				return false;
			}
			Block matching = blocks.get(id);
			if (!matching.equals(block)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Checks to make sure the board is in a legal configuration. Legality
	 * includes the presence of all the blocks with no overlaps.
	 * 
	 * @return True for all legal configurations
	 */
	public boolean checkLegal() {
	
		ArrayList<Point> positions = new ArrayList<Point>();
	
		// Check all the blocks
		if (blocks != null) {
			for (Block current : blocks) {
	
				int startRow = current.getRow();
				int startCol = current.getCol();
				int endRow = current.getLength() + startRow;
				int endCol = current.getWidth() + startCol;
	
				Point coordinate = new Point(current.getCol(), current.getRow());
				if (positions.contains(coordinate)) {
					return false;
				} else {
					positions.add(coordinate);
				}
	
				// Check for overlapping
				for (int r = startRow; r < endRow; r++) {
					for (int c = startCol; c < endCol; c++) {
						if (simpleBoard[r][c] != 0) {
							return false;
						} else {
							simpleBoard[r][c] = 1;
						}
					}
				}
	
				// Check for blocks not on the board
				if (current.getRow() < 0 || current.getCol() < 0) {
					return false;
				} else if (current.getRow() + current.getLength() > rows
						|| current.getCol() + current.getWidth() > cols) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Method to check for vacancy within the given coordinates on a simpleBoard
	 * 
	 * @param row
	 *            row in start config
	 * @param col
	 *            column in start config
	 * @param numRows
	 *            length (number of rows) of the area to check
	 * @param numCols
	 *            width (number of cols) of the area to check
	 * @return True if vacant, otherwise False
	 */
	private boolean checkVacancy(int id, int row, int col, int numRows, int numCols) {
		int endRow = row + numRows;
		int endCol = col + numCols;
		if (endRow > rows || endCol > cols || row < 0 || col < 0) {
			return false;
		}
		for (int j = col; j < endCol; j++) {
			for (int i = row; i < endRow; i++) {
				if (simpleBoard[i][j] != emptyBlock && simpleBoard[i][j] != id) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Getter method.
	 * 
	 * @return blocks of the Board object
	 */
	public ArrayList<Block> getBlocks() {
		return blocks;
	}

	/**
	 * Getter method.
	 * 
	 * @return prevMove of the Board object.
	 */
	public Integer[] getMove() {
		return prevMove;
	}

	/**
	 * Setter method. Sets the board to parent.
	 * 
	 * @param parent
	 *            Board object to be set as parent.
	 */
	public void setParent(Board parent) {
		parentConfig = parent;
	}

	/**
	 * Getter method.
	 * 
	 * @return parentConfig of the Board object.
	 */
	public Board getParent() {
		return parentConfig;
	}

	/**
	 * Getter method.
	 * 
	 * @return Board row values
	 */
	public int getRows() {
		return rows;
	}

	/**
	 * Getter method.
	 * 
	 * @return Board column values
	 */
	public int getCols() {
		return cols;
	}

	/**
	 * Getter method.
	 * 
	 * @param index
	 *            Specifies which block object to retrieve in blocks
	 * @return Returns the specified block via index.
	 */
	public Block getBlock(int index) {
		if (index < blocks.size()) {
			return blocks.get(index);
		} else {
			return null;
		}
	}

	/**
	 * Equals method determines if an input board (an object cast as a board) is
	 * the same as another board.
	 * 
	 * @param check
	 *            The board to be checked if they are equal
	 * 
	 */
	public boolean equals(Object check) {
		Board otherBoard = (Board) check;
		for (int i = 0; i < rows; i++) {
			for (int j = 0; j < cols; j++) {
				if (otherBoard.simpleBoard[i][j] != -1
						&& simpleBoard[i][j] == -1) {
					return false;
				}

				if (simpleBoard[i][j] != -1
						&& otherBoard.simpleBoard[i][j] == -1) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Inner Block class used by the Board class. Defines a Block with length,
	 * width, position, and id.
	 */
	public class Block {
		private int length, width, row, column;
		int blockId;

		// Block class constructor
		public Block(int id, int length, int width, int row, int column) {
			this.length = length;
			this.width = width;
			this.row = row;
			this.column = column;
			this.blockId = id;
		}

		/**
		 * Getter method.
		 * 
		 * @return length of a block
		 */
		public int getLength() {
			return length;
		}

		/**
		 * Getter method.
		 * 
		 * @return width of a block
		 */
		public int getWidth() {
			return width;
		}

		/**
		 * Getter method.
		 * 
		 * @return row of a block
		 */
		public int getRow() {
			return row;
		}

		/**
		 * Getter method.
		 * 
		 * @return column of a block
		 */
		public int getCol() {
			return column;
		}

		/**
		 * Checks if this block equals the block to be checked (Object cast as a
		 * block)
		 * 
		 * @param check
		 *            Block to be checked
		 * 
		 * @return True if this block equals the check block
		 */
		public boolean equals(Object check) {
			Block other = (Block) check;
			return other.row == row && other.column == column
					&& other.length == length && other.width == width;
		}
	}
}
